package com.eii.idm.config;

import com.eii.common.config.DefaultPasswordConfig;
import com.eii.common.constant.SecurityConstants;
import com.eii.common.properties.TenantProperties;
import com.eii.idm.filter.LoginProcessSetTenantFilter;
import com.eii.idm.mobile.MobileAuthenticationSecurityConfig;
import com.eii.idm.openid.OpenIdAuthenticationSecurityConfig;
import com.eii.idm.password.PasswordAuthenticationProvider;
import com.eii.idm.tenant.TenantAuthenticationSecurityConfig;
import com.eii.idm.tenant.TenantUsernamePasswordAuthenticationFilter;
import com.eii.idm.userdetails.UserDetailServiceFactory;
import com.eii.oauth2.common.token.CustomWebAuthenticationDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

/**
 * spring security配置
 * 在WebSecurityConfigurerAdapter不拦截oauth要开放的资源
 */
@Configuration
@Import(DefaultPasswordConfig.class)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private AuthenticationSuccessHandler authenticationSuccessHandler;

	@Autowired(required = false)
	private AuthenticationEntryPoint authenticationEntryPoint;

	@Autowired
	private UserDetailServiceFactory userDetailsServiceFactory;

	@Autowired
	private PasswordEncoder passwordEncoder;

	@Autowired
	private LogoutHandler logoutHandler;

	@Autowired
	private LogoutSuccessHandler logoutSuccessHandler;

	@Autowired
	private OpenIdAuthenticationSecurityConfig openIdAuthenticationSecurityConfig;

	@Autowired
	private MobileAuthenticationSecurityConfig mobileAuthenticationSecurityConfig;

	@Autowired
	private AuthenticationManager authenticationManager;

	@Autowired
	private TenantAuthenticationSecurityConfig tenantAuthenticationSecurityConfig;

	@Autowired
	private TenantProperties tenantProperties;

	@Autowired
	private AuthenticationDetailsSource<HttpServletRequest, CustomWebAuthenticationDetails> authenticationDetailsSource;

	/**
	 * 这一步的配置是必不可少的，否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
	 * @return 认证管理对象
	 */
	@Bean
    @Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

	@Bean
	public TenantUsernamePasswordAuthenticationFilter tenantAuthenticationFilter(AuthenticationManager authenticationManager) {
		TenantUsernamePasswordAuthenticationFilter filter = new TenantUsernamePasswordAuthenticationFilter();
		filter.setAuthenticationManager(authenticationManager);
		filter.setFilterProcessesUrl(SecurityConstants.OAUTH_LOGIN_PRO_URL);
		filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
		// filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(SecurityConstants.LOGIN_FAILURE_PAGE));
		filter.setAuthenticationFailureHandler(costomAuthenticationFailureHandler());
		filter.setAuthenticationDetailsSource(authenticationDetailsSource);
		return filter;
	}

	public SimpleUrlAuthenticationFailureHandler costomAuthenticationFailureHandler(){
		return new SimpleUrlAuthenticationFailureHandler(SecurityConstants.LOGIN_FAILURE_PAGE) {
			@Override
			public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
				//这里可以定位 认证错误信息
				super.onAuthenticationFailure(request, response, exception);
			}
		};
	}

/*
Spring Security如何优雅的增加OAuth2协议授权模式 see:https://juejin.cn/post/6867712199578779655

	流程分析
整个授权流程关键点分为以下两个部分：
第一部分：关于授权类型 grant_type 的解析

每种 grant_type 都会有一个对应的 TokenGranter 实现类。
所有 TokenGranter 实现类都通过 CompositeTokenGranter 中的 tokenGranters 集合存起来。
然后通过判断 grantType 参数来定位具体使用那个 TokenGranter 实现类来处理授权。

 
第二部分：关于授权登录逻辑

每种 授权方式 都会有一个对应的 AuthenticationProvider 实现类来实现。
所有 AuthenticationProvider 实现类都通过 ProviderManager 中的 providers 集合存起来。
TokenGranter 类会 new 一个 AuthenticationToken 实现类，如 UsernamePasswordAuthenticationToken 传给 ProviderManager 类。
而 ProviderManager 则通过 AuthenticationToken 来判断具体使用那个 AuthenticationProvider 实现类来处理授权。
具体的登录逻辑由 AuthenticationProvider 实现类来实现，如 DaoAuthenticationProvider。

 
3.2. 扩展分析
根据上面的流程，扩展分为以下两种场景
场景一：只对原有的授权逻辑进行增强或者扩展，如：用户名密码登录前增加图形验证码校验。
该场景需要定义一个新的 grantType 类型，并新增对应的 TokenGranter 实现类 添加扩展内容，然后加到 CompositeTokenGranter 中的 tokenGranters 集合里即可。

	 */

	@Override
	protected void configure(HttpSecurity http) throws Exception {

		//access(String) 如果给定的SpEL表达式计算结果为true，就允许访问
		//anonymous() 允许匿名用户访问
		//authenticated() 允许认证的用户进行访问
		//denyAll() 无条件拒绝所有访问
		//fullyAuthenticated() 如果用户是完整认证的话（不是通过Remember-me功能认证的），就允许访问
		//hasAuthority(String) 如果用户具备给定权限的话就允许访问
		//hasAnyAuthority(String…)如果用户具备给定权限中的某一个的话，就允许访问
		//hasRole(String) 如果用户具备给定角色(用户组)的话,就允许访问/
		//hasAnyRole(String…) 如果用户具有给定角色(用户组)中的一个的话,允许访问.
		//hasIpAddress(String 如果请求来自给定ip地址的话,就允许访问.
		//not() 对其他访问结果求反.
		//permitAll() 无条件允许访问
		//rememberMe() 如果用户是通过Remember-me功能认证的，就允许访问
		http.authorizeRequests()
					.anyRequest()
					//授权服务器关闭basic认证
                    .permitAll()
                    .and()
				.logout()
					.logoutUrl(SecurityConstants.LOGOUT_URL)
					.logoutSuccessHandler(logoutSuccessHandler)
					.addLogoutHandler(logoutHandler)
					.clearAuthentication(true)
					.and()
                .apply(openIdAuthenticationSecurityConfig)
                    .and()
				.apply(mobileAuthenticationSecurityConfig)
					.and()
				.addFilterBefore(new LoginProcessSetTenantFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf().disable()
				// 解决不允许显示在iframe的问题
				.headers().frameOptions().disable().cacheControl();

		if (tenantProperties.getEnable()) {
			//解决不同租户单点登录时角色没变化
			http.formLogin() //自定义登录界面
					.loginPage(SecurityConstants.LOGIN_PAGE) //登录界面设置
						.and()
					.addFilterAt(tenantAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class)
					.apply(tenantAuthenticationSecurityConfig);
		} else {
			http.formLogin()
					.loginPage(SecurityConstants.LOGIN_PAGE)
					.loginProcessingUrl(SecurityConstants.OAUTH_LOGIN_PRO_URL)
					.successHandler(authenticationSuccessHandler)
					.authenticationDetailsSource(authenticationDetailsSource);
		}

		// 基于密码 等模式可以无session,不支持授权码模式
		if (authenticationEntryPoint != null) {
			http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
			http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		} else {
			// 授权码模式单独处理，需要session的支持，此模式可以支持所有oauth2的认证
			http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
		}
	}

	/**
	 * 全局用户信息
	 */
	@Override
	public void configure(AuthenticationManagerBuilder auth) {
		PasswordAuthenticationProvider provider = new PasswordAuthenticationProvider();
		provider.setPasswordEncoder(passwordEncoder);
		provider.setUserDetailsServiceFactory(userDetailsServiceFactory);
		auth.authenticationProvider(provider);
	}

	/*
	public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
	}*/
}
